#!/usr/bin/python
# coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔自动同步插件
# +-------------------------------------------------------------------
# | https://www.jiulingyun.cn
# +-------------------------------------------------------------------
# | Author: yang <26577792@qq.com>
# +-------------------------------------------------------------------
import random
import sys, os, json, panel_api
import time
from ast import Str
from urllib.parse import urlparse
import uuid

import psutil

import helpr

# 设置运行目录
os.chdir("/www/server/panel")
# 添加包引用位置并引用公共包
sys.path.append("class/")
import public

# from common import dict_obj
# get = dict_obj();


# 在非命令行模式下引用面板缓存和session对象
if __name__ != '__main__':
    pass

    # 设置缓存(超时10秒) cache.set('key',value,10)
    # 获取缓存 cache.get('key')
    # 删除缓存 cache.delete('key')

    # 设置session:  session['key'] = value
    # 获取session:  value = session['key']
    # 删除session:  del(session['key'])


class remoteback_main:
    _PLUGIN_PATH = "/www/server/panel/plugin/remoteback/"
    _API_FILE = _PLUGIN_PATH + '/config/api.json'
    _INFO_FILE = _PLUGIN_PATH + '/config/sync_info.json'
    _PAN_FILE = _PLUGIN_PATH + '/config/sync_panel.json'
    _SPEED_FILE = _PLUGIN_PATH + '/config/speed.json'
    _API = None
    _API_INFO = None
    sync_config_list = None

    # 构造方法
    def __init__(self):
        path = self._PLUGIN_PATH + '/config'
        self._API = panel_api.panel_api()
        if not os.path.exists(path): os.makedirs(path, 384)
        self.sync_config_list = helpr.get_config('sync_config_list')
        if not self.sync_config_list:
            self.sync_config_list = []

    @staticmethod
    def compare_version(version1, version2):
        """
        比较两个版本号
        :param version1: 版本号1，如 '11.1.0'
        :param version2: 版本号2，如 '6.9.8'
        :return: version1 > version2 返回 1，相等返回 0，小于返回 -1
        """
        try:
            v1_parts = [int(x) for x in str(version1).split('.')]
            v2_parts = [int(x) for x in str(version2).split('.')]
            
            # 补齐版本号长度
            max_len = max(len(v1_parts), len(v2_parts))
            v1_parts += [0] * (max_len - len(v1_parts))
            v2_parts += [0] * (max_len - len(v2_parts))
            
            for i in range(max_len):
                if v1_parts[i] > v2_parts[i]:
                    return 1
                elif v1_parts[i] < v2_parts[i]:
                    return -1
            return 0
        except:
            # 如果版本号格式异常，使用字符串比较
            return 0 if version1 == version2 else (1 if version1 > version2 else -1)

    def _generate_unique_config_name(self):
        """
        生成唯一的配置名称
        使用时间戳+UUID确保唯一性
        :return: 唯一的配置名称
        """
        timestamp = int(time.time())
        unique_id = str(uuid.uuid4())[:8]  # 取UUID前8位
        config_name = f"config_{timestamp}_{unique_id}"
        
        # 双重检查，确保不重复（虽然几乎不可能）
        existing_names = {item['config_name'] for item in self.sync_config_list}
        while config_name in existing_names:
            unique_id = str(uuid.uuid4())[:8]
            config_name = f"config_{timestamp}_{unique_id}"
        
        return config_name

    @staticmethod
    def _extract_host_from_url(url):
        """
        从URL中提取主机地址（域名或IP）
        :param url: 完整URL，如 http://192.168.1.1:8888
        :return: 主机地址，如 192.168.1.1
        """
        try:
            parsed = urlparse(url)
            # 返回hostname（不含端口）
            return parsed.hostname or parsed.netloc.split(':')[0]
        except Exception as e:
            helpr.write_log_error(f"解析URL失败: {url}, 错误: {str(e)}")
            # 降级处理：尝试简单分割
            try:
                return url.split('://')[1].split(':')[0].replace('//', '')
            except:
                return url

    def index(self, args):
        return self.get_sys_info(args)

        # 获取本地服务器网站、数据库、FTP信息

    def get_src_info(self, args):
        data = {}
        data['sites'] = public.M('sites').field("id,name,path,ps,status,addtime").order("id desc").select()
        data['ftps'] = public.M('ftps').field('id,name,ps').order("id desc").select()
        data['databases'] = public.M('databases').field('id,name,ps').order("id desc").select()
        data['crontab'] = public.M('crontab').field('id,name').order("id desc").select()
        return data

    def add_config_page(self, args):
        """
        添加配置页面
        :param args:
        :return:
        """
        data = self.get_src_info(args)
        data['server_ip'] = public.GetLocalIp()
        return data

    def edit_config_page(self, args):
        if not 'config_name' in args:
            return {'status': False, 'msg': '参数错误'}
        config_name = args['config_name']
        for config in self.sync_config_list:
            if config['config_name'] == config_name:
                src_info = self.get_src_info(args)
                config['src_info'] = helpr.get_sync_info_checked(config_name, src_info)
                if 'time_type' not in config:
                    config['time_type'] = 'hour'
                return config
        return {'status': False, 'msg': '配置不存在'}

    def add_config(self, args):
        """
        添加同步配置
        :param args: 包含面板信息的参数字典
        :return: 操作结果
        """
        # 参数验证
        required_fields = ['panel', 'token', 'hour', 'title']
        if not all(field in args for field in required_fields):
            return {'status': False, 'msg': '参数错误：缺少必要字段'}
        
        if not 'time_type' in args:
            args['time_type'] = 'hour'
        
        # 验证time_type只能是minute、hour
        if args['time_type'] not in ['minute', 'hour']:
            return {'status': False, 'msg': '参数错误：time_type 只能是 minute 或 hour'}
        
        # 验证hour是否为正整数
        try:
            hour_value = int(args['hour'])
            if hour_value <= 0:
                return {'status': False, 'msg': '参数错误：同步周期必须大于0'}
            
            # 检查周期设置是否过短（给出建议）
            time_type = args.get('time_type', 'hour')
            if time_type == 'minute' and hour_value < 30:
                helpr.write_log(f"⚠️ 同步周期较短（{hour_value}分钟），建议设置30分钟以上，避免任务重叠")
            elif time_type == 'hour' and hour_value < 2:
                helpr.write_log(f"⚠️ 同步周期较短（{hour_value}小时），建议设置2小时以上，避免任务重叠")
                
        except ValueError:
            return {'status': False, 'msg': '参数错误：同步周期必须是数字'}
        
        # 检查面板连接
        check = self.check_panel_api(args)
        if not check['status']:
            return {'status': False, 'msg': f"面板连接失败，请检查面板配置信息是否正确", 'error_msg': check['msg']}
        
        # 生成唯一的配置名称
        config_name = self._generate_unique_config_name()
        
        # 提取主机地址
        host = self._extract_host_from_url(args['panel'])
        
        # 计算下次同步时间
        if args['time_type'] == 'minute':
            next_sync_seconds = int(args['hour']) * 60
        else:
            next_sync_seconds = int(args['hour']) * 60 * 60
        
        # 构建配置对象
        new_config = {
            'config_name': config_name,
            'title': args['title'],
            'panel': args['panel'],
            'token': args['token'],
            'time_type': args['time_type'],
            'host': host,
            'hour': args['hour'],
            'last_sync_time': 0,
            'next_sync_time': int(time.time()) + next_sync_seconds
        }
        
        # 设置config_name用于保存同步信息
        args['config_name'] = config_name
        
        # 先保存同步信息（验证是否能成功保存，并检查是否选择了同步数据）
        save_result = self.save_sync_info(args)
        
        # 如果保存同步信息失败（包括未选择数据），直接返回错误
        if not save_result.get('status'):
            helpr.write_log_error(f"❌ 添加配置失败: {save_result.get('msg')}")
            return save_result
        
        # 保存同步信息成功后，再添加到配置列表
        try:
            self.sync_config_list.append(new_config)
            helpr.set_config('sync_config_list', value=self.sync_config_list)
            
            total = save_result.get('total', 0)
            helpr.write_log(f"✅ 添加配置成功: {args['title']} ({config_name}), 同步 {total} 项数据")
        except Exception as e:
            helpr.write_log_error(f"❌ 保存配置列表失败: {str(e)}")
            # 删除已创建的同步信息文件
            try:
                sync_info_file = self._PLUGIN_PATH + f"config/sync_info_{config_name}.json"
                if os.path.exists(sync_info_file):
                    os.remove(sync_info_file)
            except:
                pass
            return {'status': False, 'msg': '保存配置失败'}
        
        return {'status': True, 'msg': '添加配置成功', 'data': config_name}

    def edit_config(self, args):
        """
        编辑同步配置
        :param args: 包含配置信息的参数字典
        :return: 操作结果
        """
        # 参数验证
        required_fields = ['config_name', 'panel', 'token', 'hour', 'title']
        if not all(field in args for field in required_fields):
            return {'status': False, 'msg': '参数错误：缺少必要字段'}
        
        if not 'time_type' in args:
            args['time_type'] = 'hour'
        
        # 验证time_type
        if args['time_type'] not in ['minute', 'hour']:
            return {'status': False, 'msg': '参数错误：time_type 只能是 minute 或 hour'}
        
        # 验证hour是否为正整数
        try:
            hour_value = int(args['hour'])
            if hour_value <= 0:
                return {'status': False, 'msg': '参数错误：同步周期必须大于0'}
            
            # 检查周期设置是否过短（给出建议）
            time_type = args.get('time_type', 'hour')
            if time_type == 'minute' and hour_value < 30:
                helpr.write_log(f"⚠️ 同步周期较短（{hour_value}分钟），建议设置30分钟以上，避免任务重叠")
            elif time_type == 'hour' and hour_value < 2:
                helpr.write_log(f"⚠️ 同步周期较短（{hour_value}小时），建议设置2小时以上，避免任务重叠")
                
        except ValueError:
            return {'status': False, 'msg': '参数错误：同步周期必须是数字'}
        
        config_name = args['config_name']
        
        # 查找配置
        config = None
        for item in self.sync_config_list:
            if item['config_name'] == config_name:
                config = item
                break
        
        if not config:
            return {'status': False, 'msg': f'配置不存在: {config_name}'}
        
        # 检查面板连接
        check = self.check_panel_api(args)
        if not check['status']:
            return {'status': False, 'msg': f"面板连接失败，请检查面板配置信息是否正确",
                    'error_msg': check['msg']}
        
        # 检查时间周期是否变化，如果变化需要重新计算下次同步时间
        time_changed = (config.get('hour') != args['hour'] or 
                       config.get('time_type') != args['time_type'])
        
        # 更新配置
        config['panel'] = args['panel']
        config['token'] = args['token']
        config['hour'] = args['hour']
        config['title'] = args['title']
        config['time_type'] = args['time_type']
        config['host'] = self._extract_host_from_url(args['panel'])
        
        # 备份原配置（用于失败时恢复）
        old_config = {
            'panel': config.get('panel'),
            'token': config.get('token'),
            'hour': config.get('hour'),
            'title': config.get('title'),
            'time_type': config.get('time_type'),
            'host': config.get('host'),
            'next_sync_time': config.get('next_sync_time')
        }
        
        # 如果时间周期变化，重新计算下次同步时间
        if time_changed:
            if args['time_type'] == 'minute':
                next_sync_seconds = int(args['hour']) * 60
            else:
                next_sync_seconds = int(args['hour']) * 60 * 60
            config['next_sync_time'] = int(time.time()) + next_sync_seconds
            helpr.write_log(f"⏰ 同步周期已变化，重新计算下次同步时间")
        
        # 先保存同步信息（检查是否选择了同步数据）
        save_result = self.save_sync_info(args)
        
        # 如果保存同步信息失败（包括未选择数据），恢复原配置
        if not save_result.get('status'):
            helpr.write_log_error(f"❌ 更新配置失败: {save_result.get('msg')}")
            # 恢复原配置
            config.update(old_config)
            return save_result
        
        # 保存配置列表
        try:
            helpr.set_config('sync_config_list', value=self.sync_config_list)
            
            total = save_result.get('total', 0)
            helpr.write_log(f"✅ 更新配置成功: {args['title']} ({config_name}), 同步 {total} 项数据")
        except Exception as e:
            helpr.write_log_error(f"❌ 保存配置列表失败: {str(e)}")
            # 恢复原配置
            config.update(old_config)
            return {'status': False, 'msg': '保存配置失败'}
        
        return {'status': True, 'msg': '修改配置成功', 'data': config}

    def save_sync_info(self, args):
        """
        保存同步信息
        :param args: 包含同步配置的参数字典
        :return: 操作结果
        """
        sync_info = {'sites': [], 'ftps': [], 'databases': [], 'crontab': [], 'paths': [], 'zip': True, 'speed': 0}
        
        # 解析各类同步数据
        try:
            if 'sites' in args and args['sites']:
                sync_info['sites'] = json.loads(args['sites']) if isinstance(args['sites'], str) else args['sites']
            if 'ftps' in args and args['ftps']:
                sync_info['ftps'] = json.loads(args['ftps']) if isinstance(args['ftps'], str) else args['ftps']
            if 'db' in args and args['db']:
                sync_info['databases'] = json.loads(args['db']) if isinstance(args['db'], str) else args['db']
            if 'cron' in args and args['cron']:
                sync_info['crontab'] = json.loads(args['cron']) if isinstance(args['cron'], str) else args['cron']
            if 'paths' in args and args['paths']:
                sync_info['paths'] = json.loads(args['paths']) if isinstance(args['paths'], str) else args['paths']
        except json.JSONDecodeError as e:
            helpr.write_log_error(f"❌ 解析同步数据失败: {str(e)}")
            return {'status': False, 'msg': '数据格式错误，请重新选择'}
        
        # 计算总数
        sync_info['total'] = (len(sync_info['sites']) + len(sync_info['ftps']) + 
                             len(sync_info['databases']) + len(sync_info['crontab']) + 
                             len(sync_info['paths']))
        
        # 必须至少选择一项要同步的数据
        if sync_info['total'] == 0:
            helpr.write_log(f"⚠️ 配置 {args.get('config_name', 'Unknown')} 未选择任何同步内容")
            return {'status': False, 'msg': '请至少选择一项要同步的数据（网站、FTP、数据库或计划任务）'}
        
        # 保存同步信息配置文件
        try:
            helpr.set_config(file_name=f"sync_info_{args['config_name']}", value=sync_info)
            helpr.write_log(f"💾 保存同步信息成功: {sync_info['total']} 项数据")
        except Exception as e:
            helpr.write_log_error(f"❌ 保存同步信息文件失败: {str(e)}")
            return {'status': False, 'msg': '保存同步信息失败'}
        
        return {'status': True, 'msg': '保存配置成功', 'total': sync_info['total']}

    def del_config(self, args):
        if not 'config_name' in args:
            return {'status': False, 'msg': '参数错误'}
        config_name = args['config_name']
        for config in self.sync_config_list:
            if config['config_name'] == config_name:
                self.sync_config_list.remove(config)
                helpr.set_config('sync_config_list', value=self.sync_config_list)
                sync_info_file = self._PLUGIN_PATH + f"config/sync_info_{config_name}.json"
                if os.path.exists(sync_info_file):
                    os.remove(sync_info_file)
                return {'status': True, 'msg': '删除配置成功', 'data': config}
        return {'status': False, 'msg': '配置不存在'}

    def get_sync_config_list(self, args):
        for config in self.sync_config_list:
            if config['next_sync_time'] > 0:
                config['next_sync_time'] = helpr.format_date(times=config['next_sync_time'])
            if config['last_sync_time'] > 0:
                config['last_sync_time'] = helpr.format_date(times=config['last_sync_time'])
        return {'status': True, 'msg': '获取配置列表成功', 'data': self.sync_config_list}

    def check_panel_api(self, args):
        """
        检查API状态
        :param args:
        :return:
        """
        if not 'panel' in args or not 'token' in args:
            return {'status': False, 'msg': '参数错误'}
        # 验证面板地址格式
        if not args['panel'].startswith('http://') and not args['panel'].startswith('https://'):
            return {'status': False, 'msg': '面板地址必须以 http:// 或 https:// 开头'}
        panel = panel_api.panel_api(args)
        try:
            ret = panel.send_panel('system?action=GetSystemTotal', {})
        except Exception as e:
            return public.returnMsg(False, f'目标面板连接失败，错误信息：{str(e)}<br>可能的原因：<br>1、目标面板地址错误<br>2、API密钥错误<br>3、目标面板版本低于6.9.8<br>4、未在目标API配置中放行IP白名单<br>')
        if type(ret) == Str:
            if ret.find('this exception is only raised in debug mode') != -1:
                return public.returnMsg(False, '目标服务器当前为【开发者模式】不能使用一键迁移!')
            return public.returnMsg(False, '目标面板连接失败，可能的原因：<br>1、目标面板地址错误<br>2、API密钥错误<br>3、目标面板版本低于6.9.8<br>4、未在目标API'
                                           '配置中放行IP白名单<br>')
        if not 'version' in ret:
            return {'status': False, 'msg': ret.get('msg', '未知错误'), 'data': ret}
        
        # 使用版本号比较函数进行判断
        min_version = '6.9.8'
        target_version = ret['version']
        version_compare_result = self.compare_version(target_version, min_version)
        
        # 记录版本检查信息到日志
        helpr.write_log(f"目标面板版本检查：当前版本 {target_version}，最低要求 {min_version}，比较结果：{version_compare_result}")
        
        if version_compare_result < 0:
            return public.returnMsg(False, f'目标面板版本过低，无法使用。当前版本：{target_version}，最低要求：{min_version}')
        
        return {'status': True, 'msg': f'连接成功（版本：{target_version}）', 'data': ret}

    def get_logs(self, args):
        if not 'type' in args:
            args.type = 'all'
        logs = helpr.get_logs()
        logs_error = helpr.get_logs_error()
        if not logs: logs = '暂无日志'
        if not logs_error: logs_error = '暂无日志'
        return {'status': True, 'msg': '获取日志成功', 'data': {'logs': logs, 'logs_error': logs_error}}

    def get_cron_job(self, args):
        """
        获取系统计划任务是否存在
        :return:
        """
        ret = public.ExecShell("crontab -l 2>/dev/null")
        if not ret:
            return {'status': False, 'msg': '系统计划任务不存在'}
        # 遍历查找是否存在运行'schedule.py'的任务
        for line in ret[0].split('\n'):
            if line.find('schedule.py') != -1:
                return {'status': True, 'msg': '系统计划任务存在'}
        return {'status': False, 'msg': '系统计划任务不存在'}

    def set_job_status(self, args):
        """
        设置lsyncd运行状态
        :param args: 前端传来的参数
        :return:
        """
        # 处理前端传过来的参数
        if not 'type' in args: args.type = 'start'
        py_bin = psutil.Process(os.getpid()).exe()

        # 类型限制
        if args.type not in ['start', 'stop', 'run']:
            return {'status': False, 'msg': '参数错误'}
        if args.type == 'start':
            if self.get_cron_job(args)['status']:
                return {'status': False, 'msg': '系统计划任务已存在'}
            else:

                public.ExecShell("crontab -l > /tmp/crontab.txt")
                public.ExecShell(f"echo '*/1 * * * * {py_bin} /www/server/panel/plugin/remoteback/schedule.py "
                                 f"check_sync' >>"
                                 f"/tmp/crontab.txt")
                public.ExecShell("crontab /tmp/crontab.txt")
                return {'status': True, 'msg': '设置计划任务成功'}
        if args.type == 'stop':
            if self.get_cron_job(args)['status']:
                public.ExecShell("crontab -l > /tmp/crontab.txt")
                public.ExecShell("sed -i '/schedule.py/d' /tmp/crontab.txt")
                public.ExecShell("crontab /tmp/crontab.txt")
                return {'status': True, 'msg': '删除计划任务成功'}
            else:
                return {'status': False, 'msg': '系统计划任务不存在'}
        if args.type == 'run':
            commod = f"{py_bin} /www/server/panel/plugin/remoteback/schedule.py run"
            result = public.ExecShell(commod)
            if result[1] != '':
                helpr.write_log_error(f"直接运行失败，运行命令：{commod}，错误信息：" + result[1])
                return {'status': False, 'msg': '运行失败，详情请查看错误日志', 'error': result[1]}
            return {'status': True, 'msg': '运行成功','commod': commod}

        return {'status': False, 'msg': '未知参数'}

    def get_sys_info(self, args):
        """
        获取系统信息
        :return:
        """
        # 获取系统信息
        self._API = panel_api.panel_api({
            "panel": 'https://192.168.50.141:13897',
            'token': 'CI9cfF4803FHXqlS0NqLax0U3zaFn4vX',
            'config_name': 'test'
        })
        sys_info = public.ExecShell("cat /etc/redhat-release")[0]
        sys_info += public.ExecShell("cat /etc/issue")[0]
        sys_info += public.ExecShell("uname -a")[0]
        sys_info += public.ExecShell("cat /proc/cpuinfo")[0]
        sys_info += public.ExecShell("cat/proc/meminfo")[0]
        bt_info = self._API.get_src_config()
        dst_bt_info = self._API.get_dst_config()
        return {'status': True, 'msg': '获取系统信息成功',
                'data': {'sys_info': sys_info, 'bt_info': bt_info, 'dst_bt_info': dst_bt_info}}

    def clear_logs(self, args):
        """
        清理日志
        :param args:
        :return:
        """
        if not 'type' in args:
            args.type = 'all'
        if args['type'] == 'all':
            helpr.clear_logs(logs=True, error=True)
            return {'status': True, 'msg': '清理成功'}
        if args['type'] == 'logs':
            helpr.clear_logs(logs=True, error=False)
            return {'status': True, 'msg': '清理成功'}
        if args['type'] == 'error':
            helpr.clear_logs(logs=False, error=True)
            return {'status': True, 'msg': '清理成功'}
        return {'status': False, 'msg': '未知参数'}
